// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>
#include <stdint.h>

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "content/browser/dom_storage/dom_storage_area.h"
#include "content/browser/dom_storage/dom_storage_database.h"
#include "content/browser/dom_storage/dom_storage_database_adapter.h"
#include "content/browser/dom_storage/dom_storage_task_runner.h"
#include "content/browser/dom_storage/local_storage_database_adapter.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/browser_thread.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::ASCIIToUTF16;

namespace content {

class DOMStorageAreaTest : public testing::Test {
public:
    DOMStorageAreaTest()
        : kOrigin(GURL("http://dom_storage/"))
        , kKey(ASCIIToUTF16("key"))
        , kValue(ASCIIToUTF16("value"))
        , kKey2(ASCIIToUTF16("key2"))
        , kValue2(ASCIIToUTF16("value2"))
    {
    }

    const GURL kOrigin;
    const base::string16 kKey;
    const base::string16 kValue;
    const base::string16 kKey2;
    const base::string16 kValue2;

    // Method used in the CommitTasks test case.
    void InjectedCommitSequencingTask1(
        const scoped_refptr<DOMStorageArea>& area)
    {
        // At this point the StartCommitTimer task has run and
        // the OnCommitTimer task is queued. We want to inject after
        // that.
        base::ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE,
            base::Bind(&DOMStorageAreaTest::InjectedCommitSequencingTask2,
                base::Unretained(this), area));
    }

    void InjectedCommitSequencingTask2(
        const scoped_refptr<DOMStorageArea>& area)
    {
        // At this point the OnCommitTimer has run.
        // Verify that it put a commit in flight.
        EXPECT_EQ(1, area->commit_batches_in_flight_);
        EXPECT_FALSE(area->commit_batch_.get());
        EXPECT_TRUE(area->HasUncommittedChanges());
        // Make additional change and verify that a new commit batch
        // is created for that change.
        base::NullableString16 old_value;
        EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value));
        EXPECT_TRUE(area->commit_batch_.get());
        EXPECT_EQ(1, area->commit_batches_in_flight_);
        EXPECT_TRUE(area->HasUncommittedChanges());
    }

    // Class used in the CommitChangesAtShutdown test case.
    class VerifyChangesCommittedDatabase : public DOMStorageDatabase {
    public:
        VerifyChangesCommittedDatabase() { }
        ~VerifyChangesCommittedDatabase() override
        {
            const base::string16 kKey(ASCIIToUTF16("key"));
            const base::string16 kValue(ASCIIToUTF16("value"));
            DOMStorageValuesMap values;
            ReadAllValues(&values);
            EXPECT_EQ(1u, values.size());
            EXPECT_EQ(kValue, values[kKey].string());
        }
    };

private:
    base::MessageLoop message_loop_;
};

TEST_F(DOMStorageAreaTest, DOMStorageAreaBasics)
{
    scoped_refptr<DOMStorageArea> area(
        new DOMStorageArea(1, std::string(), kOrigin, NULL, NULL));
    base::string16 old_value;
    base::NullableString16 old_nullable_value;
    scoped_refptr<DOMStorageArea> copy;

    // We don't focus on the underlying DOMStorageMap functionality
    // since that's covered by seperate unit tests.
    EXPECT_EQ(kOrigin, area->origin());
    EXPECT_EQ(1, area->namespace_id());
    EXPECT_EQ(0u, area->Length());
    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value));
    EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_nullable_value));
    EXPECT_FALSE(area->HasUncommittedChanges());

    // Verify that a copy shares the same map.
    copy = area->ShallowCopy(2, std::string());
    EXPECT_EQ(kOrigin, copy->origin());
    EXPECT_EQ(2, copy->namespace_id());
    EXPECT_EQ(area->Length(), copy->Length());
    EXPECT_EQ(area->GetItem(kKey).string(), copy->GetItem(kKey).string());
    EXPECT_EQ(area->Key(0).string(), copy->Key(0).string());
    EXPECT_EQ(copy->map_.get(), area->map_.get());

    // But will deep copy-on-write as needed.
    EXPECT_TRUE(area->RemoveItem(kKey, &old_value));
    EXPECT_NE(copy->map_.get(), area->map_.get());
    copy = area->ShallowCopy(2, std::string());
    EXPECT_EQ(copy->map_.get(), area->map_.get());
    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value));
    EXPECT_NE(copy->map_.get(), area->map_.get());
    copy = area->ShallowCopy(2, std::string());
    EXPECT_EQ(copy->map_.get(), area->map_.get());
    EXPECT_NE(0u, area->Length());
    EXPECT_TRUE(area->Clear());
    EXPECT_EQ(0u, area->Length());
    EXPECT_NE(copy->map_.get(), area->map_.get());

    // Verify that once Shutdown(), behaves that way.
    area->Shutdown();
    EXPECT_TRUE(area->is_shutdown_);
    EXPECT_FALSE(area->map_.get());
    EXPECT_EQ(0u, area->Length());
    EXPECT_TRUE(area->Key(0).is_null());
    EXPECT_TRUE(area->GetItem(kKey).is_null());
    EXPECT_FALSE(area->SetItem(kKey, kValue, &old_nullable_value));
    EXPECT_FALSE(area->RemoveItem(kKey, &old_value));
    EXPECT_FALSE(area->Clear());
}

TEST_F(DOMStorageAreaTest, BackingDatabaseOpened)
{
    const int64_t kSessionStorageNamespaceId = kLocalStorageNamespaceId + 1;
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
    const base::FilePath kExpectedOriginFilePath = temp_dir.GetPath().Append(
        DOMStorageArea::DatabaseFileNameFromOrigin(kOrigin));

    // No directory, backing should be null.
    {
        scoped_refptr<DOMStorageArea> area(
            new DOMStorageArea(kOrigin, base::FilePath(), NULL));
        EXPECT_EQ(NULL, area->backing_.get());
        EXPECT_TRUE(area->is_initial_import_done_);
        EXPECT_FALSE(base::PathExists(kExpectedOriginFilePath));
    }

    // Valid directory and origin but no session storage backing. Backing should
    // be null.
    {
        scoped_refptr<DOMStorageArea> area(
            new DOMStorageArea(kSessionStorageNamespaceId, std::string(), kOrigin,
                NULL, NULL));
        EXPECT_EQ(NULL, area->backing_.get());
        EXPECT_TRUE(area->is_initial_import_done_);

        base::NullableString16 old_value;
        EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
        ASSERT_TRUE(old_value.is_null());

        // Check that saving a value has still left us without a backing database.
        EXPECT_EQ(NULL, area->backing_.get());
        EXPECT_FALSE(base::PathExists(kExpectedOriginFilePath));
    }

    // This should set up a DOMStorageArea that is correctly backed to disk.
    {
        scoped_refptr<DOMStorageArea> area(
            new DOMStorageArea(kOrigin, temp_dir.GetPath(),
                new MockDOMStorageTaskRunner(
                    base::ThreadTaskRunnerHandle::Get().get())));

        EXPECT_TRUE(area->backing_.get());
        DOMStorageDatabase* database = static_cast<LocalStorageDatabaseAdapter*>(
            area->backing_.get())
                                           ->db_.get();
        EXPECT_FALSE(database->IsOpen());
        EXPECT_FALSE(area->is_initial_import_done_);

        // Inject an in-memory db to speed up the test.
        // We will verify that something is written into the database but not
        // that a file is written to disk - DOMStorageDatabase unit tests cover
        // that.
        area->backing_.reset(new LocalStorageDatabaseAdapter());

        // Need to write something to ensure that the database is created.
        base::NullableString16 old_value;
        EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
        ASSERT_TRUE(old_value.is_null());
        EXPECT_TRUE(area->is_initial_import_done_);
        EXPECT_TRUE(area->commit_batch_.get());
        EXPECT_EQ(0, area->commit_batches_in_flight_);

        base::RunLoop().RunUntilIdle();

        EXPECT_FALSE(area->commit_batch_.get());
        EXPECT_EQ(0, area->commit_batches_in_flight_);
        database = static_cast<LocalStorageDatabaseAdapter*>(
            area->backing_.get())
                       ->db_.get();
        EXPECT_TRUE(database->IsOpen());
        EXPECT_EQ(1u, area->Length());
        EXPECT_EQ(kValue, area->GetItem(kKey).string());

        // Verify the content made it to the in memory database.
        DOMStorageValuesMap values;
        area->backing_->ReadAllValues(&values);
        EXPECT_EQ(1u, values.size());
        EXPECT_EQ(kValue, values[kKey].string());
    }
}

TEST_F(DOMStorageAreaTest, CommitTasks)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
        kOrigin, temp_dir.GetPath(),
        new MockDOMStorageTaskRunner(base::ThreadTaskRunnerHandle::Get().get())));
    // Inject an in-memory db to speed up the test.
    area->backing_.reset(new LocalStorageDatabaseAdapter());

    // Unrelated to commits, but while we're here, see that querying Length()
    // causes the backing database to be opened and presumably read from.
    EXPECT_FALSE(area->is_initial_import_done_);
    EXPECT_EQ(0u, area->Length());
    EXPECT_TRUE(area->is_initial_import_done_);

    DOMStorageValuesMap values;
    base::NullableString16 old_value;

    // See that changes are batched up.
    EXPECT_FALSE(area->commit_batch_.get());
    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
    EXPECT_TRUE(area->HasUncommittedChanges());
    EXPECT_TRUE(area->commit_batch_.get());
    EXPECT_FALSE(area->commit_batch_->clear_all_first);
    EXPECT_EQ(1u, area->commit_batch_->changed_values.size());
    EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value));
    EXPECT_TRUE(area->commit_batch_.get());
    EXPECT_FALSE(area->commit_batch_->clear_all_first);
    EXPECT_EQ(2u, area->commit_batch_->changed_values.size());
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(area->HasUncommittedChanges());
    EXPECT_FALSE(area->commit_batch_.get());
    EXPECT_EQ(0, area->commit_batches_in_flight_);
    // Verify the changes made it to the database.
    values.clear();
    area->backing_->ReadAllValues(&values);
    EXPECT_EQ(2u, values.size());
    EXPECT_EQ(kValue, values[kKey].string());
    EXPECT_EQ(kValue2, values[kKey2].string());

    // See that clear is handled properly.
    EXPECT_TRUE(area->Clear());
    EXPECT_TRUE(area->commit_batch_.get());
    EXPECT_TRUE(area->commit_batch_->clear_all_first);
    EXPECT_TRUE(area->commit_batch_->changed_values.empty());
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(area->commit_batch_.get());
    EXPECT_EQ(0, area->commit_batches_in_flight_);
    // Verify the changes made it to the database.
    values.clear();
    area->backing_->ReadAllValues(&values);
    EXPECT_TRUE(values.empty());

    // See that if changes accrue while a commit is "in flight"
    // those will also get committed.
    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
    EXPECT_TRUE(area->HasUncommittedChanges());
    // At this point the StartCommitTimer task has been posted to the after
    // startup task queue. We inject another task in the queue that will
    // execute when the CommitChanges task is inflight. From within our
    // injected task, we'll make an additional SetItem() call and verify
    // that a new commit batch is created for that additional change.
    BrowserThread::PostAfterStartupTask(
        FROM_HERE, base::ThreadTaskRunnerHandle::Get(),
        base::Bind(&DOMStorageAreaTest::InjectedCommitSequencingTask1,
            base::Unretained(this),
            area));
    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(area->HasOneRef());
    EXPECT_FALSE(area->HasUncommittedChanges());
    // Verify the changes made it to the database.
    values.clear();
    area->backing_->ReadAllValues(&values);
    EXPECT_EQ(2u, values.size());
    EXPECT_EQ(kValue, values[kKey].string());
    EXPECT_EQ(kValue2, values[kKey2].string());
}

TEST_F(DOMStorageAreaTest, CommitChangesAtShutdown)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
    scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
        kOrigin, temp_dir.GetPath(),
        new MockDOMStorageTaskRunner(base::ThreadTaskRunnerHandle::Get().get())));

    // Inject an in-memory db to speed up the test and also to verify
    // the final changes are commited in it's dtor.
    static_cast<LocalStorageDatabaseAdapter*>(area->backing_.get())->db_.reset(new VerifyChangesCommittedDatabase());

    DOMStorageValuesMap values;
    base::NullableString16 old_value;
    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
    EXPECT_TRUE(area->HasUncommittedChanges());
    area->backing_->ReadAllValues(&values);
    EXPECT_TRUE(values.empty()); // not committed yet
    area->Shutdown();
    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(area->HasOneRef());
    EXPECT_FALSE(area->backing_.get());
    // The VerifyChangesCommittedDatabase destructor verifies values
    // were committed.

    // A second Shutdown call should be safe.
    area->Shutdown();
}

TEST_F(DOMStorageAreaTest, DeleteOrigin)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
    scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
        kOrigin, temp_dir.GetPath(),
        new MockDOMStorageTaskRunner(base::ThreadTaskRunnerHandle::Get().get())));

    // This test puts files on disk.
    base::FilePath db_file_path = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                                      ->db_->file_path();
    base::FilePath db_journal_file_path = DOMStorageDatabase::GetJournalFilePath(db_file_path);

    // Nothing bad should happen when invoked w/o any files on disk.
    area->DeleteOrigin();
    EXPECT_FALSE(base::PathExists(db_file_path));

    // Commit something in the database and then delete.
    base::NullableString16 old_value;
    area->SetItem(kKey, kValue, &old_value);
    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(base::PathExists(db_file_path));
    area->DeleteOrigin();
    EXPECT_EQ(0u, area->Length());
    EXPECT_FALSE(base::PathExists(db_file_path));
    EXPECT_FALSE(base::PathExists(db_journal_file_path));

    // Put some uncommitted changes to a non-existing database in
    // and then delete. No file ever gets created in this case.
    area->SetItem(kKey, kValue, &old_value);
    EXPECT_TRUE(area->HasUncommittedChanges());
    EXPECT_EQ(1u, area->Length());
    area->DeleteOrigin();
    EXPECT_TRUE(area->HasUncommittedChanges());
    EXPECT_EQ(0u, area->Length());
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(area->HasUncommittedChanges());
    EXPECT_FALSE(base::PathExists(db_file_path));

    // Put some uncommitted changes to a an existing database in
    // and then delete.
    area->SetItem(kKey, kValue, &old_value);
    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(base::PathExists(db_file_path));
    area->SetItem(kKey2, kValue2, &old_value);
    EXPECT_TRUE(area->HasUncommittedChanges());
    EXPECT_EQ(2u, area->Length());
    area->DeleteOrigin();
    EXPECT_TRUE(area->HasUncommittedChanges());
    EXPECT_EQ(0u, area->Length());
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(area->HasUncommittedChanges());
    // Since the area had uncommitted changes at the time delete
    // was called, the file will linger until the shutdown time.
    EXPECT_TRUE(base::PathExists(db_file_path));
    area->Shutdown();
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(base::PathExists(db_file_path));

    // A second Shutdown call should be safe.
    area->Shutdown();
}

TEST_F(DOMStorageAreaTest, PurgeMemory)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
    scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
        kOrigin, temp_dir.GetPath(),
        new MockDOMStorageTaskRunner(base::ThreadTaskRunnerHandle::Get().get())));

    // Inject an in-memory db to speed up the test.
    area->backing_.reset(new LocalStorageDatabaseAdapter());

    // Unowned ptrs we use to verify that 'purge' has happened.
    DOMStorageDatabase* original_backing = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                                               ->db_.get();
    DOMStorageMap* original_map = area->map_.get();

    // Should do no harm when called on a newly constructed object.
    EXPECT_FALSE(area->is_initial_import_done_);
    area->PurgeMemory();
    EXPECT_FALSE(area->is_initial_import_done_);
    DOMStorageDatabase* new_backing = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                                          ->db_.get();
    EXPECT_EQ(original_backing, new_backing);
    EXPECT_EQ(original_map, area->map_.get());

    // Should not do anything when commits are pending.
    base::NullableString16 old_value;
    area->SetItem(kKey, kValue, &old_value);
    EXPECT_TRUE(area->is_initial_import_done_);
    EXPECT_TRUE(area->HasUncommittedChanges());
    area->PurgeMemory();
    EXPECT_TRUE(area->is_initial_import_done_);
    EXPECT_TRUE(area->HasUncommittedChanges());
    new_backing = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                      ->db_.get();
    EXPECT_EQ(original_backing, new_backing);
    EXPECT_EQ(original_map, area->map_.get());

    // Commit the changes from above,
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(area->HasUncommittedChanges());
    new_backing = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                      ->db_.get();
    EXPECT_EQ(original_backing, new_backing);
    EXPECT_EQ(original_map, area->map_.get());

    // Should drop caches and reset database connections
    // when invoked on an area that's loaded up primed.
    area->PurgeMemory();
    EXPECT_FALSE(area->is_initial_import_done_);
    new_backing = static_cast<LocalStorageDatabaseAdapter*>(
        area->backing_.get())
                      ->db_.get();
    EXPECT_NE(original_backing, new_backing);
    EXPECT_NE(original_map, area->map_.get());
}

TEST_F(DOMStorageAreaTest, DatabaseFileNames)
{
    struct {
        const char* origin;
        const char* file_name;
        const char* journal_file_name;
    } kCases[] = {
        { "https://www.google.com/",
            "https_www.google.com_0.localstorage",
            "https_www.google.com_0.localstorage-journal" },
        { "http://www.google.com:8080/",
            "http_www.google.com_8080.localstorage",
            "http_www.google.com_8080.localstorage-journal" },
        { "file:///",
            "file__0.localstorage",
            "file__0.localstorage-journal" },
    };

    for (size_t i = 0; i < arraysize(kCases); ++i) {
        GURL origin = GURL(kCases[i].origin).GetOrigin();
        base::FilePath file_name = base::FilePath().AppendASCII(kCases[i].file_name);
        base::FilePath journal_file_name = base::FilePath().AppendASCII(kCases[i].journal_file_name);

        EXPECT_EQ(file_name,
            DOMStorageArea::DatabaseFileNameFromOrigin(origin));
        EXPECT_EQ(origin,
            DOMStorageArea::OriginFromDatabaseFileName(file_name));
        EXPECT_EQ(journal_file_name,
            DOMStorageDatabase::GetJournalFilePath(file_name));
    }

    // Also test some DOMStorageDatabase::GetJournalFilePath cases here.
    base::FilePath parent = base::FilePath().AppendASCII("a").AppendASCII("b");
    EXPECT_EQ(
        parent.AppendASCII("file-journal"),
        DOMStorageDatabase::GetJournalFilePath(parent.AppendASCII("file")));
    EXPECT_EQ(
        base::FilePath().AppendASCII("-journal"),
        DOMStorageDatabase::GetJournalFilePath(base::FilePath()));
    EXPECT_EQ(
        base::FilePath().AppendASCII(".extensiononly-journal"),
        DOMStorageDatabase::GetJournalFilePath(
            base::FilePath().AppendASCII(".extensiononly")));
}

TEST_F(DOMStorageAreaTest, RateLimiter)
{
    // Limit to 1000 samples per second
    DOMStorageArea::RateLimiter rate_limiter(
        1000, base::TimeDelta::FromSeconds(1));

    // No samples have been added so no time/delay should be needed.
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeTimeNeeded());
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta()));
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta::FromDays(1)));

    // Add a seconds worth of samples.
    rate_limiter.add_samples(1000);
    EXPECT_EQ(base::TimeDelta::FromSeconds(1),
        rate_limiter.ComputeTimeNeeded());
    EXPECT_EQ(base::TimeDelta::FromSeconds(1),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta()));
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta::FromSeconds(1)));
    EXPECT_EQ(base::TimeDelta::FromMilliseconds(250),
        rate_limiter.ComputeDelayNeeded(
            base::TimeDelta::FromMilliseconds(750)));
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(
            base::TimeDelta::FromDays(1)));

    // And another half seconds worth.
    rate_limiter.add_samples(500);
    EXPECT_EQ(base::TimeDelta::FromMilliseconds(1500),
        rate_limiter.ComputeTimeNeeded());
    EXPECT_EQ(base::TimeDelta::FromMilliseconds(1500),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta()));
    EXPECT_EQ(base::TimeDelta::FromMilliseconds(500),
        rate_limiter.ComputeDelayNeeded(base::TimeDelta::FromSeconds(1)));
    EXPECT_EQ(base::TimeDelta::FromMilliseconds(750),
        rate_limiter.ComputeDelayNeeded(
            base::TimeDelta::FromMilliseconds(750)));
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(
            base::TimeDelta::FromMilliseconds(1500)));
    EXPECT_EQ(base::TimeDelta(),
        rate_limiter.ComputeDelayNeeded(
            base::TimeDelta::FromDays(1)));
}

} // namespace content
